<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="exj6F" id="exj6F"><span data-lake-id="u91f92ccd" id="u91f92ccd">典型回答</span></h1>
  <p data-lake-id="u62f8e361" id="u62f8e361"><span data-lake-id="u1723b11b" id="u1723b11b">​</span><br></p>
  <p data-lake-id="ub0038071" id="ub0038071"><span data-lake-id="u2b13705e" id="u2b13705e">事务管理在系统开发中是不可缺少的一部分，Spring提供了很好事务管理机制，主要分为编程式事务和声明式事务两种。</span></p>
  <p data-lake-id="ud3f520c0" id="ud3f520c0"><br></p>
  <h4 data-lake-id="3bd0ee3e" id="3bd0ee3e"><span data-lake-id="ud160b020" id="ud160b020">编程式事务</span></h4>
  <p data-lake-id="u44e4925c" id="u44e4925c"><br></p>
  <p data-lake-id="u6cf961b7" id="u6cf961b7"><span data-lake-id="u24af2f60" id="u24af2f60">基于底层的API，如PlatformTransactionManager、TransactionDefinition 和 TransactionTemplate 等核心接口，开发者完全可以通过编程的方式来进行事务管理。</span></p>
  <p data-lake-id="u532ec756" id="u532ec756"><br></p>
  <p data-lake-id="u65728bf9" id="u65728bf9"><strong><span data-lake-id="u12bf6d35" id="u12bf6d35">编程式事务方式需要是开发者在代码中手动的管理事务的开启、提交、回滚等操作。</span></strong></p>
  <p data-lake-id="ude3e119f" id="ude3e119f"><br></p>
  <pre lang="java"><code>
public void test() {
      TransactionDefinition def = new DefaultTransactionDefinition();
      TransactionStatus status = transactionManager.getTransaction(def);

       try {
         // 事务操作
         // 事务提交
         transactionManager.commit(status);
      } catch (DataAccessException e) {
         // 事务回滚
         transactionManager.rollback(status);
         throw e;
      }
}
</code></pre>
  <p data-lake-id="u39e0b83f" id="u39e0b83f"><span data-lake-id="u1fd0f4d4" id="u1fd0f4d4">​</span><br></p>
  <p data-lake-id="ud07996e4" id="ud07996e4"><span data-lake-id="u5620f99c" id="u5620f99c">当然，我们也可以使用Spring中提供的TransactionTemplate来实现编程式事务。</span></p>
  <p data-lake-id="u51086fc1" id="u51086fc1"><span data-lake-id="u9d65f60e" id="u9d65f60e">​</span><br></p>
  <pre lang="java"><code>
    @Autowired
    protected TransactionTemplate transactionTemplate;

  	public void test(){
      return transactionTemplate.execute(transactionStatus -&gt; {
         //事务操作
      });
    }

		public void test1(){
      transactionTemplate.execute(new TransactionCallbackWithoutResult() {
          @Override
          protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
              
          }
      });
    }
    
</code></pre>
  <p data-lake-id="u4d98327e" id="u4d98327e"><br></p>
  <p data-lake-id="u80a385fa" id="u80a385fa"><span data-lake-id="u69d53adb" id="u69d53adb">如以上代码，开发者可以通过API自己控制事务。</span></p>
  <p data-lake-id="u2d87a801" id="u2d87a801"><br></p>
  <h4 data-lake-id="1e2d1932" id="1e2d1932"><span data-lake-id="u15cdd285" id="u15cdd285">声明式事务</span></h4>
  <p data-lake-id="ub2b9f24e" id="ub2b9f24e"><br></p>
  <p data-lake-id="ud17c8165" id="ud17c8165"><strong><span data-lake-id="u9453c6d0" id="u9453c6d0">声明式事务管理方法允许开发者配置的帮助下来管理事务，而不需要依赖底层API进行硬编码。开发者可以只使用注解或基于配置的 XML 来管理事务。</span></strong></p>
  <p data-lake-id="u945d6b58" id="u945d6b58"><br></p>
  <pre lang="java"><code>
@Transactional
public void test() {
     // 事务操作  
}
</code></pre>
  <p data-lake-id="u6ebd91cb" id="u6ebd91cb"><br></p>
  <p data-lake-id="udd866111" id="udd866111"><span data-lake-id="u5ca292ba" id="u5ca292ba">如上，使用@Transactional即可给test方法增加事务控制。</span></p>
  <p data-lake-id="u565093b7" id="u565093b7"><br></p>
  <h1 data-lake-id="V3D9i" id="V3D9i"><span data-lake-id="u0cd2cbaa" id="u0cd2cbaa">扩展知识</span></h1>
  <p data-lake-id="u28d79549" id="u28d79549"><br></p>
  <h2 data-lake-id="fmInS" id="fmInS"><span data-lake-id="u94e1f637" id="u94e1f637">声明式事务的优点</span></h2>
  <p data-lake-id="u0ca7cb57" id="u0ca7cb57"><br></p>
  <p data-lake-id="uce5604bf" id="uce5604bf"><span data-lake-id="uada946d0" id="uada946d0">通过上面的例子，其实我们可以很容易的看出来，声明式事务帮助我们节省了很多代码，他会自动帮我们进行事务的开启、提交以及回滚等操作，把程序员从事务管理中解放出来。</span></p>
  <p data-lake-id="u1e3105fe" id="u1e3105fe"><br></p>
  <p data-lake-id="u692f5c0a" id="u692f5c0a"><strong><span data-lake-id="ub2fbb652" id="ub2fbb652">声明式事务管理使用了 AOP 实现的，本质就是在目标方法执行前后进行拦截。</span></strong><span data-lake-id="uf2197628" id="uf2197628">在目标方法执行前加入或创建一个事务，在执行方法执行后，根据实际情况选择提交或是回滚事务。</span></p>
  <p data-lake-id="u05dd157d" id="u05dd157d"><br></p>
  <p data-lake-id="u450b9b3b" id="u450b9b3b"><strong><span data-lake-id="u6d2f4897" id="u6d2f4897">使用这种方式，对代码没有侵入性，方法内只需要写业务逻辑就可以了。</span></strong></p>
  <p data-lake-id="u5f42b84a" id="u5f42b84a"><br></p>
  <p data-lake-id="u0ccd8670" id="u0ccd8670"><span data-lake-id="u47276b0b" id="u47276b0b">但是，声明式事务真的有这么好么？倒也不见得。</span></p>
  <p data-lake-id="ud01c3314" id="ud01c3314"><br></p>
  <h2 data-lake-id="VYeck" id="VYeck"><span data-lake-id="ucf61c6b2" id="ucf61c6b2">声明式事务的粒度问题</span></h2>
  <p data-lake-id="uffd3c0a7" id="uffd3c0a7"><br></p>
  <p data-lake-id="u86b859bb" id="u86b859bb"><span data-lake-id="u0112ee14" id="u0112ee14">首先，</span><strong><span data-lake-id="u1188d34f" id="u1188d34f">声明式事务有一个局限，那就是他的最小粒度要作用在方法上。</span></strong></p>
  <p data-lake-id="u865acf5d" id="u865acf5d"><br></p>
  <p data-lake-id="u70184be7" id="u70184be7"><span data-lake-id="ua604df08" id="ua604df08">也就是说，如果想要给一部分代码块增加事务的话，那就需要把这个部分代码块单独独立出来作为一个方法。</span></p>
  <p data-lake-id="u175f7df0" id="u175f7df0"><br></p>
  <p data-lake-id="u908e0883" id="u908e0883"><span data-lake-id="u61f51b1b" id="u61f51b1b">但是，正是因为这个粒度问题，本人并不建议过度的使用声明式事务。</span></p>
  <p data-lake-id="ubf1e15ab" id="ubf1e15ab"><br></p>
  <p data-lake-id="ub4a3ef8d" id="ub4a3ef8d"><span data-lake-id="u7b1d97f3" id="u7b1d97f3">首先，因为声明式事务是通过注解的，有些时候还可以通过配置实现，这就会导致一个问题，那就是这个事务有可能被开发者忽略。</span></p>
  <p data-lake-id="u0170d460" id="u0170d460"><br></p>
  <p data-lake-id="uff84d16c" id="uff84d16c"><span data-lake-id="u645a96cd" id="u645a96cd">事务被忽略了有什么问题呢？</span></p>
  <p data-lake-id="u1e672091" id="u1e672091"><br></p>
  <p data-lake-id="u2b3a3dff" id="u2b3a3dff"><strong><span data-lake-id="uac9c9521" id="uac9c9521">首先，如果开发者没有注意到一个方法是被事务嵌套的，那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。</span></strong></p>
  <p data-lake-id="u43cff506" id="u43cff506"><br></p>
  <p data-lake-id="u97348ec3" id="u97348ec3"><span data-lake-id="uf278f8ae" id="uf278f8ae">我们知道，这些操作如果被包在事务中，有两个问题：</span></p>
  <p data-lake-id="udd33a838" id="udd33a838"><br></p>
  <p data-lake-id="uc7c63e66" id="uc7c63e66"><span data-lake-id="u5fe84982" id="u5fe84982">1、这些操作自身是无法回滚的，这就会导致数据的不一致。可能RPC调用成功了，但是本地事务回滚了，可是RPC调用无法回滚了。</span></p>
  <p data-lake-id="u6922944d" id="u6922944d"><br></p>
  <p data-lake-id="u23b72409" id="u23b72409"><span data-lake-id="u78e75671" id="u78e75671">2、在事务中有远程调用，就会拉长整个事务。那么就会导致本事务的数据库连接一直被占用，那么如果类似操作过多，就会导致数据库连接池耗尽。</span></p>
  <p data-lake-id="udec609ca" id="udec609ca"><br></p>
  <p data-lake-id="uf1edc956" id="uf1edc956"><span data-lake-id="u910adf21" id="u910adf21">有些时候，即使没有在事务中进行远程操作，但是有些人还是可能会不经意的进行一些内存操作，如运算。或者如果遇到分库分表的情况，有可能不经意间进行跨库操作。</span></p>
  <p data-lake-id="ueab509d7" id="ueab509d7"><br></p>
  <p data-lake-id="u6c678beb" id="u6c678beb"><span data-lake-id="u20d1d48d" id="u20d1d48d">但是如果是编程式事务的话，业务代码中就会清清楚楚看到什么地方开启事务，什么地方提交，什么时候回滚。这样有人改这段代码的时候，就会强制他考虑要加的代码是否应该方法事务内。</span></p>
  <p data-lake-id="u871d28e2" id="u871d28e2"><br></p>
  <p data-lake-id="ue0fa23dd" id="ue0fa23dd"><span data-lake-id="u5a27c758" id="u5a27c758">有些人可能会说，已经有了声明式事务，但是写代码的人没注意，这能怪谁。</span></p>
  <p data-lake-id="u52c89097" id="u52c89097"><br></p>
  <p data-lake-id="u3fa55e70" id="u3fa55e70"><span data-lake-id="uc8cc8288" id="uc8cc8288">话虽然是这么说，但是我们还是希望可以通过一些机制或者规范，降低这些问题发生的概率。</span></p>
  <p data-lake-id="u8590ffb6" id="u8590ffb6"><br></p>
  <p data-lake-id="u6a4e929c" id="u6a4e929c"><span data-lake-id="ud57bc851" id="ud57bc851">比如建议大家使用编程式事务，而不是声明式事务。因为，作者工作这么多年来，发生过不止一次开发者没注意到声明式事务而导致的故障。</span></p>
  <p data-lake-id="ud2fc569c" id="ud2fc569c"><br></p>
  <p data-lake-id="u911bf4c7" id="u911bf4c7"><span data-lake-id="u4d4a0cc8" id="u4d4a0cc8">因为有些时候，声明式事务确实不够明显。</span></p>
  <p data-lake-id="uf6e6ef1f" id="uf6e6ef1f"><br></p>
  <h2 data-lake-id="BcPf1" id="BcPf1"><span data-lake-id="u4cd32d75" id="u4cd32d75">声明式事务用不对容易失效</span></h2>
  <p data-lake-id="ua0c80e6e" id="ua0c80e6e"><br></p>
  <p data-lake-id="u51d0ccaa" id="u51d0ccaa"><span data-lake-id="u1fab1de0" id="u1fab1de0">除了事务的粒度问题，还有一个问题那就是声明式事务虽然看上去帮我们简化了很多代码，但是一旦没用对，也很容易导致事务失效。</span></p>
  <p data-lake-id="u0b316f5c" id="u0b316f5c"><br></p>
  <p data-lake-id="u4104b3df" id="u4104b3df"><span data-lake-id="ud764c8f2" id="ud764c8f2">如以下几种场景就可能导致声明式事务失效：</span></p>
  <p data-lake-id="u4b37a104" id="u4b37a104"><br></p>
  <p data-lake-id="u0d6e0192" id="u0d6e0192"><span data-lake-id="u0bc50024" id="u0bc50024">1、</span><a href="/Transactional " data-lake-id="u9dc40cff" id="u9dc40cff"><span data-lake-id="ub0da56ef" id="ub0da56ef">@Transactional </span></a><span data-lake-id="u08cf1ae8" id="u08cf1ae8"> 应用在非 public 修饰的方法上 <br></span><span data-lake-id="u6a2b89a5" id="u6a2b89a5">2、</span><a href="/Transactional " data-lake-id="ufb562a1f" id="ufb562a1f"><span data-lake-id="u60f9f52b" id="u60f9f52b">@Transactional </span></a><span data-lake-id="uc0984e2d" id="uc0984e2d"> 注解属性 propagation 设置错误 <br></span><span data-lake-id="u4ff6173b" id="u4ff6173b">3、</span><a href="/Transactional " data-lake-id="u4838c73f" id="u4838c73f"><span data-lake-id="ufdcaee00" id="ufdcaee00">@Transactional </span></a><span data-lake-id="uc6f98691" id="uc6f98691"> 注解属性 rollbackFor 设置错误 <br></span><span data-lake-id="u0b9731b7" id="u0b9731b7">4、同一个类中方法调用，导致@Transactional失效<br></span><span data-lake-id="u97531458" id="u97531458">5、异常被catch捕获导致@Transactional失效<br></span><span data-lake-id="u93c88908" id="u93c88908">6、数据库引擎不支持事务</span></p>
  <p data-lake-id="u5f1befea" id="u5f1befea"><br></p>
  <p data-lake-id="uc758e224" id="uc758e224"><span data-lake-id="u55bd0d59" id="u55bd0d59">以上几个问题，如果使用编程式事务的话，很多都是可以避免的。</span></p>
  <p data-lake-id="u9ca6e66c" id="u9ca6e66c"><br></p>
  <p data-lake-id="ud5534899" id="ud5534899"><span data-lake-id="uf1d444e7" id="uf1d444e7">使用声明事务失效的问题我们发生过很多次。不知道大家有没有遇到过，我是实际遇到过的</span></p>
  <p data-lake-id="u6e095019" id="u6e095019"><br></p>
  <p data-lake-id="ua5718f24" id="ua5718f24"><span data-lake-id="uee974408" id="uee974408">因为Spring的事务是基于AOP实现的，但是在代码中，有时候我们会有很多切面，不同的切面可能会来处理不同的事情，多个切面之间可能会有相互影响。</span></p>
  <p data-lake-id="udb6506bc" id="udb6506bc"><br></p>
  <p data-lake-id="u2dfb664e" id="u2dfb664e"><span data-lake-id="u6b17c461" id="u6b17c461">在之前的一个项目中，我就发现我们的Service层的事务全都失效了，一个SQL执行失败后并没有回滚，排查下来才发现，是因为一位同事新增了一个切面，这个切面里面做个异常的统一捕获，导致事务的切面没有捕获到异常，导致事务无法回滚。</span></p>
  <p data-lake-id="uae747758" id="uae747758"><span data-lake-id="u0cdc37b7" id="u0cdc37b7">​</span><br></p>
  <blockquote data-lake-id="u6157f28b" id="u6157f28b">
   <p data-lake-id="u09f05917" id="u09f05917"><span data-lake-id="ud2def19b" id="ud2def19b">后来解决的方式可以设置切面的执行顺序，比如使用@Order或者实现Ordered接口等，但是我们这里没这么做，而是经过代码分析后把异常捕获的这部分移除了，异常抛给上游再处理。</span></p>
   <p data-lake-id="ufe33cf6b" id="ufe33cf6b"><span data-lake-id="u54fd439f" id="u54fd439f">之所以没有通过设置顺序解决，主要是因为这玩意不好控制，完以后面新增了别的注解之类的还得再改动，所以经过分析之后这里异常捕获并非必要，就这么改了。</span></p>
  </blockquote>
  <p data-lake-id="u8f3fa7ad" id="u8f3fa7ad"><br></p>
  <p data-lake-id="u329f5981" id="u329f5981"><span data-lake-id="ueebc3f25" id="ueebc3f25">这样的问题，发生过不止一次，而且不容易被发现。</span></p>
  <p data-lake-id="u65ddcff2" id="u65ddcff2"><br></p>
  <p data-lake-id="ubba68e28" id="ubba68e28"><span data-lake-id="u5109549f" id="u5109549f">很多人还是会说，说到底还是自己能力不行，对事务理解不透彻，用错了能怪谁。</span></p>
  <p data-lake-id="u9206ccbb" id="u9206ccbb"><br></p>
  <p data-lake-id="u32d458bf" id="u32d458bf"><span data-lake-id="u192ad59b" id="u192ad59b">但是我还是那句话，</span><strong><span data-lake-id="ua46eecad" id="ua46eecad">我们确实无法保证所有人的能力都很高</span></strong><span data-lake-id="u74c585c5" id="u74c585c5">，也无法要求所有开发者都能不出错。我们能做的就是，尽量可以通过机制或者规范，来避免或者降低这些问题发生的概率。</span></p>
  <p data-lake-id="u44f11867" id="u44f11867"><br></p>
  <p data-lake-id="ud41ded10" id="ud41ded10"><span data-lake-id="uba8e72df" id="uba8e72df">其实，如果大家有认真看过阿里巴巴出的那份Java开发手册的话，其实就能发现，其中的很多规约并不是完完全全容易被人理解，有些也比较生硬，但是其实，这些规范都是从无数个坑里爬出来的开发者们总结出来的。</span></p>
  <p data-lake-id="ubd2dd21d" id="ubd2dd21d"><br></p>
  <p data-lake-id="u8d0bacc9" id="u8d0bacc9"><span data-lake-id="ua0416dde" id="ua0416dde">关于@Transactional的用法，规约中也有提到过，只不过规约中的观点没有我这么鲜明：</span></p>
  <p data-lake-id="u8d01e368" id="u8d01e368"><br></p>
  <p data-lake-id="ucfce14b7" id="ucfce14b7"><img src="http://www.hollischuang.com/wp-content/uploads/2020/10/Jietu20201011-171246.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_46%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u8773426b" id="u8773426b"><br></p>
  <p data-lake-id="uc23493a2" id="uc23493a2"><br></p>
 </body>
</html>